<!DOCTYPE html>


<html lang="en">


<head>
  <meta charset="utf-8" />
    
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
  <title>
    springboot中dubbo服务引用 |  一曲秋殇
  </title>
  <meta name="generator" content="hexo-theme-ayer">
  
  <link rel="shortcut icon" href="/images/myfavicon.ico" />
  
  
<link rel="stylesheet" href="/dist/main.css">

  
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/Shen-Yu/cdn/css/remixicon.min.css">

  
<link rel="stylesheet" href="/css/custom.css">

  
  
<script src="https://cdn.jsdelivr.net/npm/pace-js@1.0.2/pace.min.js"></script>

  
  

  

</head>

</html>

<body>
  <div id="app">
    
      
    <main class="content on">
      <section class="outer">
  <article
  id="post-dubbo/springboot中dubbo服务引用"
  class="article article-type-post"
  itemscope
  itemprop="blogPost"
  data-scroll-reveal
>
  <div class="article-inner">
    
    <header class="article-header">
       
<h1 class="article-title sea-center" style="border-left:0" itemprop="name">
  springboot中dubbo服务引用
</h1>
 

    </header>
     
    <div class="article-meta">
      <a href="/dubbo/springboot%E4%B8%ADdubbo%E6%9C%8D%E5%8A%A1%E5%BC%95%E7%94%A8/" class="article-date">
  <time datetime="2021-12-13T05:51:40.042Z" itemprop="datePublished">2021-12-13</time>
</a> 
  <div class="article-category">
    <a class="article-category-link" href="/categories/%E5%90%8E%E5%8F%B0/">后台</a> / <a class="article-category-link" href="/categories/%E5%90%8E%E5%8F%B0/java/">java</a> / <a class="article-category-link" href="/categories/%E5%90%8E%E5%8F%B0/java/dubbo/">dubbo</a>
  </div>
  
<div class="word_count">
    <span class="post-time">
        <span class="post-meta-item-icon">
            <i class="ri-quill-pen-line"></i>
            <span class="post-meta-item-text"> Word count:</span>
            <span class="post-count">4k</span>
        </span>
    </span>

    <span class="post-time">
        &nbsp; | &nbsp;
        <span class="post-meta-item-icon">
            <i class="ri-book-open-line"></i>
            <span class="post-meta-item-text"> Reading time≈</span>
            <span class="post-count">18 min</span>
        </span>
    </span>
</div>
 
    </div>
      
    <div class="tocbot"></div>




  
    <div class="article-entry" itemprop="articleBody">
       
  <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/hint.css/2.4.1/hint.min.css"><blockquote>
<p>前面用三篇文字分析了服务导出，这篇文章来分析下服务引用的相关的东西<br>以下分析是基于dubbo2.7.8</p>
</blockquote>
<h4 id="1-服务引用的触发原理"><a href="#1-服务引用的触发原理" class="headerlink" title="1. 服务引用的触发原理"></a>1. 服务引用的触发原理</h4><hr>
<p> 先回顾下服务导出的时是怎样扫描dubbo服务注册到spring bean容器的。<br> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">在DubboAutoConfiguration中定义了一个ServiceClassPostProcessor类型的bean，该bean实现了BeanDefinitionRegistryPostProcessor接口</span><br><span class="line">在spring加载bean定义的时候会调用该接口的postProcessBeanDefinitionRegistry方法，而ServiceClassPostProcessor重写了该方法将标注了DubboService或Service注解的bean添加到bean定义中最终加载成Spring的bean</span><br></pre></td></tr></table></figure></p>
<p> 而服务的引用和服务导出不一样，是要将Spring bean中带有DubboReference或Reference注解的属性注入值<br> 下面来分析下DubboReference或Reference属性是如何被注入值的<br> 在DubboAutoConfiguration上有一个@EnableDubboConfig注解，该注解@Import了DubboConfigConfigurationRegistrar这个类<br> <figure class="highlight java"><figcaption><span>DubboConfigConfigurationRegistrar.class</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">DubboConfigConfigurationRegistrar</span> <span class="keyword">implements</span> <span class="title">ImportBeanDefinitionRegistrar</span> </span>&#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">registerBeanDefinitions</span><span class="params">(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry)</span> </span>&#123;</span><br><span class="line">        AnnotationAttributes attributes = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(EnableDubboConfig.class.getName()));</span><br><span class="line">        <span class="keyword">boolean</span> multiple = attributes.getBoolean(<span class="string">&quot;multiple&quot;</span>);</span><br><span class="line">        <span class="comment">// 注册DubboConfigConfiguration.Single.class中的那些bean</span></span><br><span class="line">        registerBeans(registry, DubboConfigConfiguration.Single.class);</span><br><span class="line">        <span class="keyword">if</span> (multiple) &#123; </span><br><span class="line">            registerBeans(registry, DubboConfigConfiguration.Multiple.class);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 该方法中会注册一些公用的bean</span></span><br><span class="line">        registerCommonBeans(registry);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br>查看 registerCommonBeans方法代码</p>
<figure class="highlight java"><figcaption><span>DubboConfigConfigurationRegistrar.class</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">void</span> <span class="title">registerCommonBeans</span><span class="params">(BeanDefinitionRegistry registry)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Since 2.5.7 Register @Reference Annotation Bean Processor as an infrastructure Bean</span></span><br><span class="line">    <span class="comment">// 注入了一个ReferenceAnnotationBeanPostProcessor的注解后置处理器</span></span><br><span class="line">    registerInfrastructureBean(registry, ReferenceAnnotationBeanPostProcessor.BEAN_NAME,</span><br><span class="line">            ReferenceAnnotationBeanPostProcessor.class);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Since 2.7.4 [Feature] https://github.com/apache/dubbo/issues/5093</span></span><br><span class="line">    registerInfrastructureBean(registry, DubboConfigAliasPostProcessor.BEAN_NAME,</span><br><span class="line">            DubboConfigAliasPostProcessor.class);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Since 2.7.5 Register DubboLifecycleComponentApplicationListener as an infrastructure Bean</span></span><br><span class="line">    registerInfrastructureBean(registry, DubboLifecycleComponentApplicationListener.BEAN_NAME,</span><br><span class="line">            DubboLifecycleComponentApplicationListener.class);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Since 2.7.4 Register DubboBootstrapApplicationListener as an infrastructure Bean</span></span><br><span class="line">    registerInfrastructureBean(registry, DubboBootstrapApplicationListener.BEAN_NAME,</span><br><span class="line">            DubboBootstrapApplicationListener.class);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Since 2.7.6 Register DubboConfigDefaultPropertyValueBeanPostProcessor as an infrastructure Bean</span></span><br><span class="line">    registerInfrastructureBean(registry, DubboConfigDefaultPropertyValueBeanPostProcessor.BEAN_NAME,</span><br><span class="line">            DubboConfigDefaultPropertyValueBeanPostProcessor.class);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>上面的registerCommonBeans方法注入了一个ReferenceAnnotationBeanPostProcessor后置处理器，该处理器继承自AbstractAnnotationBeanPostProcessor类</p>
<figure class="highlight java"><figcaption><span>ReferenceAnnotationBeanPostProcessor.class</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">ReferenceAnnotationBeanPostProcessor</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="comment">//指定哪些注解标识的字段是需要执行依赖注入的，被这些注解标识的属性获取依赖的时候是调用该下面的doGetInjectedBean方法进行获取</span></span><br><span class="line">    <span class="keyword">super</span>(DubboReference.class, Reference.class, com.alibaba.dubbo.config.annotation.Reference.class);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">protected</span> Object <span class="title">doGetInjectedBean</span><span class="params">(AnnotationAttributes attributes, Object bean, String beanName, Class&lt;?&gt; injectedType,</span></span></span><br><span class="line"><span class="function"><span class="params">                                   InjectionMetadata.InjectedElement injectedElement)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line"> </span><br><span class="line">    String referencedBeanName = buildReferencedBeanName(attributes, injectedType);</span><br><span class="line"></span><br><span class="line">    String referenceBeanName = getReferenceBeanName(attributes, injectedType);</span><br><span class="line"></span><br><span class="line">    <span class="comment">//主要这一步构建referenceBean的时候会先去referenceBeanCache缓存中取（已经构建过的referenceBean会被缓存下来，key为@DubboReference注解+服务名称）</span></span><br><span class="line">    <span class="comment">//所以在重复引用同一个服务且@DubboReference注解参数什么都一样的情况下是使用的同一个referenceBean对象</span></span><br><span class="line">    ReferenceBean referenceBean = buildReferenceBeanIfAbsent(referenceBeanName, attributes, injectedType);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">boolean</span> localServiceBean = isLocalServiceBean(referencedBeanName, referenceBean, attributes);</span><br><span class="line"></span><br><span class="line">    prepareReferenceBean(referencedBeanName, referenceBean, localServiceBean);</span><br><span class="line"></span><br><span class="line">    registerReferenceBean(referencedBeanName, referenceBean, attributes, localServiceBean, injectedType);</span><br><span class="line"></span><br><span class="line">    cacheInjectedReferenceBean(referenceBean, injectedElement);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">//上面的代码构建了一个ReferenceBean对象，执行get会返回对象中的ref属性，如果该属性没有值则会执行init方法创建该对象并赋值给ref属性</span></span><br><span class="line">    <span class="keyword">return</span> referenceBean.get();</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>
<h4 id="2-引用对象的初始化"><a href="#2-引用对象的初始化" class="headerlink" title="2. 引用对象的初始化"></a>2. 引用对象的初始化</h4><hr>
<p>引用对象的初始化都在init方法中，该方法代码如下：</p>
<figure class="highlight java"><figcaption><span>ReferenceConfig.java</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title">init</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (initialized) &#123;</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (bootstrap == <span class="keyword">null</span>) &#123;</span><br><span class="line">        bootstrap = DubboBootstrap.getInstance();</span><br><span class="line">        <span class="comment">//和服务暴露一样，也会执行DubboBootstrap的初始化（启动注册中心和元数据中心等操作）</span></span><br><span class="line">        bootstrap.init();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    checkAndUpdateSubConfigs();</span><br><span class="line"></span><br><span class="line">    checkStubAndLocal(interfaceClass);</span><br><span class="line">    ConfigValidationUtils.checkMock(interfaceClass, <span class="keyword">this</span>);</span><br><span class="line"></span><br><span class="line">    Map&lt;String, String&gt; map = <span class="keyword">new</span> HashMap&lt;String, String&gt;();</span><br><span class="line">    </span><br><span class="line">    <span class="comment">//省略代码无数......,主要是收集一些参数添加到map中</span></span><br><span class="line">   </span><br><span class="line">    <span class="comment">//将map中收集到的一些参数添加到元数据对象中，最终会写入到元数据中心</span></span><br><span class="line">    serviceMetadata.getAttachments().putAll(map);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">//非常重要的一步，创建引用对象赋值给ReferenceBean#ref属性，ref对象其实就是一个代理对象，详见2.1</span></span><br><span class="line">    ref = createProxy(map);</span><br><span class="line"></span><br><span class="line">    serviceMetadata.setTarget(ref);</span><br><span class="line">    serviceMetadata.addAttribute(PROXY_CLASS_REF, ref);</span><br><span class="line">    ConsumerModel consumerModel = repository.lookupReferredService(serviceMetadata.getServiceKey());</span><br><span class="line">    consumerModel.setProxyObject(ref);</span><br><span class="line">    consumerModel.init(attributes);</span><br><span class="line"></span><br><span class="line">    initialized = <span class="keyword">true</span>;</span><br><span class="line"></span><br><span class="line">    checkInvokerAvailable();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 触发ReferenceConfigInitializedEvent事件</span></span><br><span class="line">    dispatch(<span class="keyword">new</span> ReferenceConfigInitializedEvent(<span class="keyword">this</span>, invoker));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h5 id="2-1-创建服务接口代理对象"><a href="#2-1-创建服务接口代理对象" class="headerlink" title="2.1 创建服务接口代理对象"></a>2.1 创建服务接口代理对象</h5><figure class="highlight java"><figcaption><span>ReferenceConfig.class</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@SuppressWarnings(&#123;&quot;unchecked&quot;, &quot;rawtypes&quot;, &quot;deprecation&quot;&#125;)</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> T <span class="title">createProxy</span><span class="params">(Map&lt;String, String&gt; map)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (shouldJvmRefer(map)) &#123;</span><br><span class="line">        <span class="comment">//如果是虚拟机内部的服务引用，则协议为injvm</span></span><br><span class="line">        URL url = <span class="keyword">new</span> URL(LOCAL_PROTOCOL, LOCALHOST_VALUE, <span class="number">0</span>, interfaceClass.getName()).addParameters(map);</span><br><span class="line">        <span class="comment">//injvm协议直接获取invoker对象</span></span><br><span class="line">        invoker = REF_PROTOCOL.refer(interfaceClass, url);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        urls.clear();</span><br><span class="line">        <span class="keyword">if</span> (url != <span class="keyword">null</span> &amp;&amp; url.length() &gt; <span class="number">0</span>) &#123; </span><br><span class="line">            <span class="comment">//进入这里，说明用户自己直接指定了服务或指定了注册中心url</span></span><br><span class="line">            <span class="comment">//比如用户在@DubboReference注解中指定url=dubbo://xxx，则不需要通过注册中心直接获取服务</span></span><br><span class="line">            <span class="comment">//可以同时指定多个url，用&quot;;&quot;号分割</span></span><br><span class="line">            String[] us = SEMICOLON_SPLIT_PATTERN.split(url);</span><br><span class="line">            <span class="keyword">if</span> (us != <span class="keyword">null</span> &amp;&amp; us.length &gt; <span class="number">0</span>) &#123;</span><br><span class="line">                <span class="keyword">for</span> (String u : us) &#123;</span><br><span class="line">                    URL url = URL.valueOf(u);</span><br><span class="line">                    <span class="keyword">if</span> (StringUtils.isEmpty(url.getPath())) &#123;</span><br><span class="line">                        url = url.setPath(interfaceName);</span><br><span class="line">                    &#125;</span><br><span class="line">                    <span class="keyword">if</span> (UrlUtils.isRegistry(url)) &#123;</span><br><span class="line">                        <span class="comment">//指定的是注册中心地址</span></span><br><span class="line">                        urls.add(url.addParameterAndEncoded(REFER_KEY, StringUtils.toQueryString(map)));</span><br><span class="line">                    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                        urls.add(ClusterUtils.mergeUrl(url, map));</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123; </span><br><span class="line">            <span class="keyword">if</span> (!LOCAL_PROTOCOL.equalsIgnoreCase(getProtocol())) &#123;</span><br><span class="line">                <span class="comment">//进入到这里则说明用户没有指定url，则从注册中心配置获取url列表</span></span><br><span class="line">                checkRegistry();</span><br><span class="line">                List&lt;URL&gt; us = ConfigValidationUtils.loadRegistries(<span class="keyword">this</span>, <span class="keyword">false</span>);</span><br><span class="line">                <span class="keyword">if</span> (CollectionUtils.isNotEmpty(us)) &#123;</span><br><span class="line">                    <span class="keyword">for</span> (URL u : us) &#123;</span><br><span class="line">                        URL monitorUrl = ConfigValidationUtils.loadMonitor(<span class="keyword">this</span>, u);</span><br><span class="line">                        <span class="keyword">if</span> (monitorUrl != <span class="keyword">null</span>) &#123;</span><br><span class="line">                            map.put(MONITOR_KEY, URL.encode(monitorUrl.toFullString()));</span><br><span class="line">                        &#125;</span><br><span class="line">                        urls.add(u.addParameterAndEncoded(REFER_KEY, StringUtils.toQueryString(map)));</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="keyword">if</span> (urls.isEmpty()) &#123;</span><br><span class="line">                    <span class="keyword">throw</span> <span class="keyword">new</span> IllegalStateException(<span class="string">&quot;&quot;</span>);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (urls.size() == <span class="number">1</span>) &#123;</span><br><span class="line">            <span class="comment">//不管是直接指定还是从注册中心配置读取，如果只有一个url，直接通过协议对应的拓展获取invoker对象，详见2.1.1</span></span><br><span class="line">            invoker = REF_PROTOCOL.refer(interfaceClass, urls.get(<span class="number">0</span>));</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="comment">//如果有多个url时，则循序这些url取获取invoker添加到invokers列表中</span></span><br><span class="line">            List&lt;Invoker&lt;?&gt;&gt; invokers = <span class="keyword">new</span> ArrayList&lt;Invoker&lt;?&gt;&gt;();</span><br><span class="line">            URL registryURL = <span class="keyword">null</span>;</span><br><span class="line">            <span class="keyword">for</span> (URL url : urls) &#123;</span><br><span class="line">                invokers.add(REF_PROTOCOL.refer(interfaceClass, url));</span><br><span class="line">                <span class="keyword">if</span> (UrlUtils.isRegistry(url)) &#123;</span><br><span class="line">                    registryURL = url; </span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> (registryURL != <span class="keyword">null</span>) &#123; </span><br><span class="line">                <span class="comment">//当urls中存在注册中心url则取最后一个url的cluster参数值获取集群实现，如果没有指定则默认为ZoneAwareCluster</span></span><br><span class="line">                <span class="comment">//其实到这里一般是使用了多注册中心配置，所以默认使用ZoneAwareCluster实现，ZoneAwareCluster是2.7.5新加的集群接口，主要是处理多注册中心集群方案时做注册中心选择用的</span></span><br><span class="line">                <span class="comment">//在多注册中心场景下，消费端调用服务时会先选择使用哪个注册中心，对应注册中心的选择策略有如下四种：</span></span><br><span class="line">                <span class="comment">//1.指定优先级（在注册中心配置中加preferred=&quot;true&quot;） 2.同 zone 优先（在注册中心配置中加zone=&quot;shenzhen&quot;，&quot;shenzhen&quot;为zone key） 3.权重轮询（在注册中心配置中加 weight=&quot;100&quot;） 4.选择第一个可用的即可（默认策略）</span></span><br><span class="line">                String cluster = registryURL.getParameter(CLUSTER_KEY, ZoneAwareCluster.NAME);</span><br><span class="line">                <span class="comment">//通过上面循序获取的invokers列表构建一个静态服务目录对象StaticDirectory，然后通过Cluster将该服务目录进行封装返回一个invoker对象</span></span><br><span class="line">                invoker = Cluster.getCluster(cluster, <span class="keyword">false</span>).join(<span class="keyword">new</span> StaticDirectory(registryURL, invokers));</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123; </span><br><span class="line">                String cluster = CollectionUtils.isNotEmpty(invokers)</span><br><span class="line">                        ? (invokers.get(<span class="number">0</span>).getUrl() != <span class="keyword">null</span> ? invokers.get(<span class="number">0</span>).getUrl().getParameter(CLUSTER_KEY, ZoneAwareCluster.NAME) : Cluster.DEFAULT): Cluster.DEFAULT;</span><br><span class="line">                invoker = Cluster.getCluster(cluster).join(<span class="keyword">new</span> StaticDirectory(invokers));</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (logger.isInfoEnabled()) &#123;</span><br><span class="line">        logger.info(<span class="string">&quot;Refer dubbo service &quot;</span> + interfaceClass.getName() + <span class="string">&quot; from url &quot;</span> + invoker.getUrl());</span><br><span class="line">    &#125;</span><br><span class="line">    String metadata = map.get(METADATA_KEY);</span><br><span class="line">    WritableMetadataService metadataService = WritableMetadataService.getExtension(metadata == <span class="keyword">null</span> ? DEFAULT_METADATA_STORAGE_TYPE : metadata);</span><br><span class="line">    <span class="keyword">if</span> (metadataService != <span class="keyword">null</span>) &#123;</span><br><span class="line">        URL consumerURL = <span class="keyword">new</span> URL(CONSUMER_PROTOCOL, map.remove(REGISTER_IP_KEY), <span class="number">0</span>, map.get(INTERFACE_KEY), map);</span><br><span class="line">        <span class="comment">//将当引用元数据信息写入到元数据中心</span></span><br><span class="line">        metadataService.publishServiceDefinition(consumerURL);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//通过invoker对象生成服务接口的代理，详见2.1.2</span></span><br><span class="line">    <span class="keyword">return</span> (T) PROXY_FACTORY.getProxy(invoker, ProtocolUtils.isGeneric(generic));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h6 id="2-1-1-通过Protocol-refer方法获取Invoker对象"><a href="#2-1-1-通过Protocol-refer方法获取Invoker对象" class="headerlink" title="2.1.1 通过Protocol#refer方法获取Invoker对象"></a>2.1.1 通过Protocol#refer方法获取Invoker对象</h6><p>当只有一个注册中心的时候，会通过<code>REF_PROTOCOL.refer(interfaceClass, urls.get(0));</code>获取Invoker对象，这个协议可能是dubbo（是dubbo时表示是直连），也可能是register，此处我们以常见的register为例进行说明<br>在协为register时所获取到拓展实现为RegistryProtocol（和服务暴露的时候一样，只不过服务暴露是进入export方法）<br>RegistryProtocol#refer代码如下：</p>
<figure class="highlight java"><figcaption><span>RegistryProtocol.class</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> &lt;T&gt; <span class="function">Invoker&lt;T&gt; <span class="title">refer</span><span class="params">(Class&lt;T&gt; type, URL url)</span> <span class="keyword">throws</span> RpcException </span>&#123;</span><br><span class="line">    <span class="comment">//通过url生成一个具体注册协议的url，比如使用zookeeper作为注册中心，则生成的url为 zookeeper://xxx</span></span><br><span class="line">    url = getRegistryUrl(url);</span><br><span class="line">    <span class="comment">//获取Registry对象，这个和服务导出-3中获取Registry一样，如使用的zookeeper协议则最终会返回一个ZookeeperRegistry对象，用于实现对注册中心的相关操作</span></span><br><span class="line">    Registry registry = registryFactory.getRegistry(url);</span><br><span class="line">    <span class="keyword">if</span> (RegistryService.class.equals(type)) &#123;</span><br><span class="line">        <span class="keyword">return</span> proxyFactory.getInvoker((T) registry, type, url);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//获取url中refer参数的值解码后转成map，解码后的refer值如下：</span></span><br><span class="line">    <span class="comment">//application=c3-consumer&amp;dubbo=2.0.2&amp;init=false&amp;interface=com.study.api.OrderService&amp;metadata=remote&amp;metadata-type=remote&amp;methods=getOrder&amp;pid=7732&amp;qos.enable=false&amp;register.ip=192.168.56.1&amp;release=2.7.8&amp;side=consumer&amp;sticky=false&amp;timestamp=1618267897669</span></span><br><span class="line">    Map&lt;String, String&gt; qs = StringUtils.parseQueryString(url.getParameterAndDecoded(REFER_KEY));</span><br><span class="line">    String group = qs.get(GROUP_KEY);</span><br><span class="line">    <span class="keyword">if</span> (group != <span class="keyword">null</span> &amp;&amp; group.length() &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">if</span> ((COMMA_SPLIT_PATTERN.split(group)).length &gt; <span class="number">1</span> || <span class="string">&quot;*&quot;</span>.equals(group)) &#123;</span><br><span class="line">            <span class="keyword">return</span> doRefer(Cluster.getCluster(MergeableCluster.NAME), registry, type, url);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//获取Cluster拓展实现，在url中没有指定的情况下默认是获取key为failover的拓展实现 FailoverCluster，该对象会被MockClusterWrapper装饰</span></span><br><span class="line">    Cluster cluster = Cluster.getCluster(qs.get(CLUSTER_KEY));</span><br><span class="line">    <span class="comment">//获取到registry和cluster后最终调用doRefer方法进行生成Invoker对象，下面详细进行分析</span></span><br><span class="line">    <span class="keyword">return</span> doRefer(cluster, registry, type, url);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>doRefer方法如下：</p>
<figure class="highlight java"><figcaption><span>RegistryProtocol.class</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> &lt;T&gt; <span class="function">Invoker&lt;T&gt; <span class="title">doRefer</span><span class="params">(Cluster cluster, Registry registry, Class&lt;T&gt; type, URL url)</span> </span>&#123;</span><br><span class="line">    <span class="comment">//实例化一个服务目录对象RegistryDirectory，详见2.1.1.1</span></span><br><span class="line">    RegistryDirectory&lt;T&gt; directory = <span class="keyword">new</span> RegistryDirectory&lt;T&gt;(type, url);</span><br><span class="line">    directory.setRegistry(registry);</span><br><span class="line">    directory.setProtocol(protocol);</span><br><span class="line">    Map&lt;String, String&gt; parameters = <span class="keyword">new</span> HashMap&lt;String, String&gt;(directory.getConsumerUrl().getParameters());</span><br><span class="line">    URL subscribeUrl = <span class="keyword">new</span> URL(CONSUMER_PROTOCOL, parameters.remove(REGISTER_IP_KEY), <span class="number">0</span>, type.getName(), parameters);</span><br><span class="line">    <span class="comment">//通过register参数判断是否需要需要注册url到注册中心，没指定默认为注册</span></span><br><span class="line">    <span class="keyword">if</span> (directory.isShouldRegister()) &#123;</span><br><span class="line">        directory.setRegisteredConsumerUrl(subscribeUrl);</span><br><span class="line">        <span class="comment">//执行注册操作，注册成功后会在服务目录的consumers目录中出现consumer://xxxx</span></span><br><span class="line">        registry.register(directory.getRegisteredConsumerUrl());</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//构建路由链，详见2.1.1.2</span></span><br><span class="line">    directory.buildRouterChain(subscribeUrl);</span><br><span class="line">    <span class="comment">//订阅服务提供者，详见2.1.1.3</span></span><br><span class="line">    directory.subscribe(toSubscribeUrl(subscribeUrl));</span><br><span class="line">    <span class="comment">//获取Invoke对象，详见2.1.1.4</span></span><br><span class="line">    Invoker&lt;T&gt; invoker = cluster.join(directory);</span><br><span class="line">    List&lt;RegistryProtocolListener&gt; listeners = findRegistryProtocolListeners(url);</span><br><span class="line">    <span class="keyword">if</span> (CollectionUtils.isEmpty(listeners)) &#123;</span><br><span class="line">        <span class="keyword">return</span> invoker;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//下面主要是在有配置RegistryProtocolListener拓展的情况下进行进行通知服务引用事件</span></span><br><span class="line">    RegistryInvokerWrapper&lt;T&gt; registryInvokerWrapper = <span class="keyword">new</span> RegistryInvokerWrapper&lt;&gt;(directory, cluster, invoker);</span><br><span class="line">    <span class="keyword">for</span> (RegistryProtocolListener listener : listeners) &#123;</span><br><span class="line">        listener.onRefer(<span class="keyword">this</span>, registryInvokerWrapper);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> registryInvokerWrapper;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h6 id="2-1-1-1-创建服务目录对象RegistryDirectory"><a href="#2-1-1-1-创建服务目录对象RegistryDirectory" class="headerlink" title="2.1.1.1 创建服务目录对象RegistryDirectory"></a>2.1.1.1 创建服务目录对象RegistryDirectory</h6><p>服务目录中会存储提供者Invoke列表，其构造如下：</p>
<figure class="highlight java"><figcaption><span>RegistryDirectory.class</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">RegistryDirectory</span><span class="params">(Class&lt;T&gt; serviceType, URL url)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">super</span>(url);</span><br><span class="line">    <span class="keyword">if</span> (serviceType == <span class="keyword">null</span>) &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> IllegalArgumentException(<span class="string">&quot;service type is null.&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    shouldRegister = !ANY_VALUE.equals(url.getServiceInterface()) &amp;&amp; url.getParameter(REGISTER_KEY, <span class="keyword">true</span>);</span><br><span class="line">    shouldSimplified = url.getParameter(SIMPLIFIED_KEY, <span class="keyword">false</span>);</span><br><span class="line">    <span class="keyword">if</span> (url.getServiceKey() == <span class="keyword">null</span> || url.getServiceKey().length() == <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> IllegalArgumentException(<span class="string">&quot;registry serviceKey is null.&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">this</span>.serviceType = serviceType;</span><br><span class="line">    <span class="keyword">this</span>.serviceKey = url.getServiceKey();</span><br><span class="line">    <span class="keyword">this</span>.queryMap = StringUtils.parseQueryString(url.getParameterAndDecoded(REFER_KEY));</span><br><span class="line">    <span class="keyword">this</span>.overrideDirectoryUrl = <span class="keyword">this</span>.directoryUrl = turnRegistryUrlToConsumerUrl(url);</span><br><span class="line">    String group = directoryUrl.getParameter(GROUP_KEY, <span class="string">&quot;&quot;</span>);</span><br><span class="line">    <span class="keyword">this</span>.multiGroup = group != <span class="keyword">null</span> &amp;&amp; (ANY_VALUE.equals(group) || group.contains(<span class="string">&quot;,&quot;</span>));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h6 id="2-1-1-2-创建RouterChain对象设置到服务目录中"><a href="#2-1-1-2-创建RouterChain对象设置到服务目录中" class="headerlink" title="2.1.1.2 创建RouterChain对象设置到服务目录中"></a>2.1.1.2 创建RouterChain对象设置到服务目录中</h6><p>RouterChain构造如下：</p>
<figure class="highlight java"><figcaption><span>RouterChain.class</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="title">RouterChain</span><span class="params">(URL url)</span> </span>&#123;</span><br><span class="line">    <span class="comment">//获取激活的路由工厂，内置的有四个，分别是MockRouterFactory，TagRouterFactory，AppRouterFactory，ServiceRouterFactory</span></span><br><span class="line">    List&lt;RouterFactory&gt; extensionFactories = ExtensionLoader.getExtensionLoader(RouterFactory.class)</span><br><span class="line">            .getActivateExtension(url, <span class="string">&quot;router&quot;</span>);</span><br><span class="line">    <span class="comment">//这一步主要是通过路由工厂的getRouter方法获取路由，最终将所有工厂获取的Router放入一个路由列表</span></span><br><span class="line">    List&lt;Router&gt; routers = extensionFactories.stream()</span><br><span class="line">            .map(factory -&gt; factory.getRouter(url))</span><br><span class="line">            .collect(Collectors.toList());</span><br><span class="line">    <span class="comment">//这一步主要是将上面的路由列表放入到RouterChain#routers属性中，并进行排序</span></span><br><span class="line">    initWithRouters(routers);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>内置的四种路由对应的实现类如下：</p>
<ol>
<li>MockInvokersSelector（用于处理请求的attachments属性中携带invocation.need.mock=true时使用mock服务）</li>
<li>TagRouter（标签路由）</li>
<li>AppRouter（应用级别条件路由）</li>
<li>ServiceRouter（服务级别条件路由）</li>
</ol>
<p>关于TagRouter和AppRouter动态路由监听原理如下图所示：<br><img src="http://img.luobiao.vip/img/20210508075555.png"><br><a target="_blank" rel="noopener" href="https://www.processon.com/view/link/6095d3e31e08533e428ae4fb">原图链接</a></p>
<p>关于标签路由执行逻辑如下图所示：<br><img src="http://img.luobiao.vip/img/20210508075814.png"><br><a target="_blank" rel="noopener" href="https://www.processon.com/view/link/6095d4635653bb147737b538">原图链接</a></p>
<h6 id="2-1-1-3-订阅对应服务提供者及配置中心配置变更"><a href="#2-1-1-3-订阅对应服务提供者及配置中心配置变更" class="headerlink" title="2.1.1.3 订阅对应服务提供者及配置中心配置变更"></a>2.1.1.3 订阅对应服务提供者及配置中心配置变更</h6><p>订阅方法如下：</p>
<figure class="highlight java"><figcaption><span>RegistryDirectory.class</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">subscribe</span><span class="params">(URL url)</span> </span>&#123;</span><br><span class="line">    setConsumerUrl(url);</span><br><span class="line">    <span class="comment">//这一步主要是监听注册中心的/dubbo/config/dubbo/应用名称.configurators配置的变化</span></span><br><span class="line">    CONSUMER_CONFIGURATION_LISTENER.addNotifyListener(<span class="keyword">this</span>);</span><br><span class="line">    <span class="comment">//这一步主要是监听注册中心的/dubbo/config/dubbo/com.study.api.OrderService::.configurators配置的变化</span></span><br><span class="line">    serviceConfigurationListener = <span class="keyword">new</span> ReferenceConfigurationListener(<span class="keyword">this</span>, url);</span><br><span class="line">    <span class="comment">//订阅对应服务提供者providers目录变更，如：/dubbo/com.study.api.OrderService/providers</span></span><br><span class="line">    registry.subscribe(url, <span class="keyword">this</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h6 id="2-1-1-4-通过Cluster-join方法获取Invoker对象"><a href="#2-1-1-4-通过Cluster-join方法获取Invoker对象" class="headerlink" title="2.1.1.4 通过Cluster#join方法获取Invoker对象"></a>2.1.1.4 通过Cluster#join方法获取Invoker对象</h6><blockquote>
<p>Cluster#join方法将最终返回一个ClusterInvoker实现，该实现中处理了路由及负载均衡等逻辑</p>
</blockquote>
<p>在refer方法中调用doRefer方法的时候有传入一个Cluster拓展实现，默认为FailoverCluster，Cluster拓展实现会被MockClusterWrapper 装饰<br>所以会执行MockClusterWrapper#join方法获取Invoker，该方法代码如下：</p>
<figure class="highlight java"><figcaption><span>MockClusterWrapper.class</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> &lt;T&gt; <span class="function">Invoker&lt;T&gt; <span class="title">join</span><span class="params">(Directory&lt;T&gt; directory)</span> <span class="keyword">throws</span> RpcException </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> MockClusterInvoker&lt;T&gt;(directory,</span><br><span class="line">            <span class="keyword">this</span>.cluster.join(directory));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">MockClusterInvoker</span><span class="params">(Directory&lt;T&gt; directory, Invoker&lt;T&gt; invoker)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">this</span>.directory = directory;</span><br><span class="line">    <span class="keyword">this</span>.invoker = invoker;</span><br><span class="line">&#125;    </span><br></pre></td></tr></table></figure>
<p>从上面代码可知MockClusterInvoker最终还是调用的FailoverCluster#join方法进行获取一个ClusterInvoker链，然后保存到MockClusterInvoker#invoker属性中<br>FailoverCluster#join方法的实现是在父类AbstractCluster中，代码如下</p>
<figure class="highlight java"><figcaption><span>AbstractCluster.class</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> &lt;T&gt; <span class="function">Invoker&lt;T&gt; <span class="title">join</span><span class="params">(Directory&lt;T&gt; directory)</span> <span class="keyword">throws</span> RpcException </span>&#123;</span><br><span class="line">    <span class="comment">//先是调用doJoin方法获取FailoverCluster的Invoker对象FailoverClusterInvoker，然后调用buildClusterInterceptors方法添加拦截器，最终返回一个ClusterInvoker链</span></span><br><span class="line">    <span class="keyword">return</span> buildClusterInterceptors(doJoin(directory), directory.getUrl().getParameter(REFERENCE_INTERCEPTOR_KEY));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>buildClusterInterceptors方法代码如下：</p>
<figure class="highlight java"><figcaption><span>AbstractCluster.class</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> &lt;T&gt; <span class="function">Invoker&lt;T&gt; <span class="title">buildClusterInterceptors</span><span class="params">(AbstractClusterInvoker&lt;T&gt; clusterInvoker, String key)</span> </span>&#123;</span><br><span class="line">    <span class="comment">//具体的ClusterInvoker实现放在ClusterInvoker链的最后，如果没有获取到激活的ClusterInterceptor拓展则这个链上就只有这一个节点</span></span><br><span class="line">    AbstractClusterInvoker&lt;T&gt; last = clusterInvoker;</span><br><span class="line">    <span class="comment">//获取激活的ClusterInterceptor拓展，默认会获取到ConsumerContextClusterInterceptor实现</span></span><br><span class="line">    List&lt;ClusterInterceptor&gt; interceptors = ExtensionLoader.getExtensionLoader(ClusterInterceptor.class).getActivateExtension(clusterInvoker.getUrl(), key);</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> (!interceptors.isEmpty()) &#123;</span><br><span class="line">        <span class="comment">//组装ClusterInvoker链</span></span><br><span class="line">        <span class="keyword">for</span> (<span class="keyword">int</span> i = interceptors.size() - <span class="number">1</span>; i &gt;= <span class="number">0</span>; i--) &#123;</span><br><span class="line">            <span class="keyword">final</span> ClusterInterceptor interceptor = interceptors.get(i);</span><br><span class="line">            <span class="keyword">final</span> AbstractClusterInvoker&lt;T&gt; next = last;</span><br><span class="line">            last = <span class="keyword">new</span> InterceptorInvokerNode&lt;&gt;(clusterInvoker, interceptor, next);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> last;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>关于ClusterInvoker链相关类图如下：<br><img src="http://img.luobiao.vip/img/20210417175430.png"><br><a target="_blank" rel="noopener" href="https://www.processon.com/view/link/6048cecbf346fb616f717e15">原图链接</a></p>
<p>默Cluster拓展实FailoverCluster最终组装的ClusterInvoker链结构如下：<br><img src="http://img.luobiao.vip/img/20210417180531.png"><br><a target="_blank" rel="noopener" href="https://www.processon.com/view/link/6048cecbf346fb616f717e15">原图链接</a></p>
<p>在消费者调用服务提供者的时候就是调用MockClusterInvoker#invoker方法，该方法中主要是做了mock逻辑判断，代码如下</p>
<figure class="highlight java"><figcaption><span>MockClusterInvoker.class</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> Result <span class="title">invoke</span><span class="params">(Invocation invocation)</span> <span class="keyword">throws</span> RpcException </span>&#123;</span><br><span class="line">    Result result = <span class="keyword">null</span>;</span><br><span class="line"></span><br><span class="line">    String value = getUrl().getMethodParameter(invocation.getMethodName(), MOCK_KEY, Boolean.FALSE.toString()).trim();</span><br><span class="line">    <span class="keyword">if</span> (value.length() == <span class="number">0</span> || <span class="string">&quot;false&quot;</span>.equalsIgnoreCase(value)) &#123;</span><br><span class="line">        <span class="comment">//没有mock参数的话会进入这里调用ClusterInvoker链的invoke方法</span></span><br><span class="line">        result = <span class="keyword">this</span>.invoker.invoke(invocation);</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (value.startsWith(<span class="string">&quot;force&quot;</span>)) &#123;</span><br><span class="line">        <span class="comment">//force:direct mock</span></span><br><span class="line">        result = doMockInvoke(invocation, <span class="keyword">null</span>);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">//fail-mock</span></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            result = <span class="keyword">this</span>.invoker.invoke(invocation);</span><br><span class="line">            <span class="comment">//fix:#4585</span></span><br><span class="line">            <span class="keyword">if</span>(result.getException() != <span class="keyword">null</span> &amp;&amp; result.getException() <span class="keyword">instanceof</span> RpcException)&#123;</span><br><span class="line">                RpcException rpcException= (RpcException)result.getException();</span><br><span class="line">                <span class="keyword">if</span>(rpcException.isBiz())&#123;</span><br><span class="line">                    <span class="keyword">throw</span>  rpcException;</span><br><span class="line">                &#125;<span class="keyword">else</span> &#123;</span><br><span class="line">                    result = doMockInvoke(invocation, rpcException);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">        &#125; <span class="keyword">catch</span> (RpcException e) &#123;</span><br><span class="line">            result = doMockInvoke(invocation, e);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>不是mock的话MockClusterInvoker#invoker方法中会调用InterceptorInvokerNode#invoke，该方法代码如下：</p>
<figure class="highlight java"><figcaption><span>InterceptorInvokerNode.class</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> Result <span class="title">invoke</span><span class="params">(Invocation invocation)</span> <span class="keyword">throws</span> RpcException </span>&#123;</span><br><span class="line">    Result asyncResult;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="comment">//先执行当前拦截器的before方法</span></span><br><span class="line">        interceptor.before(next, invocation);</span><br><span class="line">        <span class="comment">//执行拦截器的interceptor方法，intercept方法中会执行传入next的invoke方法，所以如果不是尾结点（即具体的集群实现如FailoverClusterInvoker）会递归执行InterceptorInvokerNode#invoke，直到执行到具体的集群实现返回调用结果</span></span><br><span class="line">        asyncResult = interceptor.intercept(next, invocation);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">        <span class="keyword">if</span> (interceptor <span class="keyword">instanceof</span> ClusterInterceptor.Listener) &#123;</span><br><span class="line">            ClusterInterceptor.Listener listener = (ClusterInterceptor.Listener) interceptor;</span><br><span class="line">            listener.onError(e, clusterInvoker, invocation);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">throw</span> e;</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        <span class="comment">//先执行当前拦截器的after方法</span></span><br><span class="line">        interceptor.after(next, invocation);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> asyncResult.whenCompleteWithContext((r, t) -&gt; &#123;</span><br><span class="line">        <span class="keyword">if</span> (interceptor <span class="keyword">instanceof</span> ClusterInterceptor.Listener) &#123;</span><br><span class="line">            ClusterInterceptor.Listener listener = (ClusterInterceptor.Listener) interceptor;</span><br><span class="line">            <span class="keyword">if</span> (t == <span class="keyword">null</span>) &#123;</span><br><span class="line">                listener.onMessage(r, clusterInvoker, invocation);</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                listener.onError(t, clusterInvoker, invocation);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>从上面的代码可以看出集群拦截器的实现原理，具体的集群实现细节后面的服务调用章节在展开分析</p>
<h6 id="2-1-2-通过invoker对象生成服务接口的代理"><a href="#2-1-2-通过invoker对象生成服务接口的代理" class="headerlink" title="2.1.2 通过invoker对象生成服务接口的代理"></a>2.1.2 通过invoker对象生成服务接口的代理</h6><p>在默认情况下使用的是代理工厂是JavassistProxyFactory，其getProxy方法如下</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> &lt;T&gt; <span class="function">T <span class="title">getProxy</span><span class="params">(Invoker&lt;T&gt; invoker, Class&lt;?&gt;[] interfaces)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> (T) Proxy.getProxy(interfaces).newInstance(<span class="keyword">new</span> InvokerInvocationHandler(invoker));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>可以看出通过InvokerInvocationHandler类对invoker对象进行了适配，InvokerInvocationHandler构造方法如下</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">InvokerInvocationHandler</span><span class="params">(Invoker&lt;?&gt; handler)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">this</span>.invoker = handler;</span><br><span class="line">    String serviceKey = invoker.getUrl().getServiceKey();</span><br><span class="line">    <span class="keyword">if</span> (serviceKey != <span class="keyword">null</span>) &#123;</span><br><span class="line">        <span class="keyword">this</span>.consumerModel = ApplicationModel.getConsumerModel(serviceKey);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>所有的对服务接口的请求最终都通过代理会进入InvokerInvocationHandler#invoke方法，该方法中最后会调用invoker对象的invoke方法，也就是会进入上面的MockClusterInvoker#invoker<br>具体的服务调用过程在服务调用章节再展开分析</p>
<h5 id="2-2-触发ReferenceConfigInitializedEvent事件"><a href="#2-2-触发ReferenceConfigInitializedEvent事件" class="headerlink" title="2.2 触发ReferenceConfigInitializedEvent事件"></a>2.2 触发ReferenceConfigInitializedEvent事件</h5><link rel="stylesheet" href="/css/spoiler.css" type="text/css"><script src="/js/spoiler.js" type="text/javascript" async></script> 
      <!-- reward -->
      
    </div>
    

    <!-- copyright -->
    
    <footer class="article-footer">
       
<div class="share-btn">
      <span class="share-sns share-outer">
        <i class="ri-share-forward-line"></i>
        分享
      </span>
      <div class="share-wrap">
        <i class="arrow"></i>
        <div class="share-icons">
          
          <a class="weibo share-sns" href="javascript:;" data-type="weibo">
            <i class="ri-weibo-fill"></i>
          </a>
          <a class="weixin share-sns wxFab" href="javascript:;" data-type="weixin">
            <i class="ri-wechat-fill"></i>
          </a>
          <a class="qq share-sns" href="javascript:;" data-type="qq">
            <i class="ri-qq-fill"></i>
          </a>
          <a class="douban share-sns" href="javascript:;" data-type="douban">
            <i class="ri-douban-line"></i>
          </a>
          <!-- <a class="qzone share-sns" href="javascript:;" data-type="qzone">
            <i class="icon icon-qzone"></i>
          </a> -->
          
          <a class="facebook share-sns" href="javascript:;" data-type="facebook">
            <i class="ri-facebook-circle-fill"></i>
          </a>
          <a class="twitter share-sns" href="javascript:;" data-type="twitter">
            <i class="ri-twitter-fill"></i>
          </a>
          <a class="google share-sns" href="javascript:;" data-type="google">
            <i class="ri-google-fill"></i>
          </a>
        </div>
      </div>
</div>

<div class="wx-share-modal">
    <a class="modal-close" href="javascript:;"><i class="ri-close-circle-line"></i></a>
    <p>扫一扫，分享到微信</p>
    <div class="wx-qrcode">
      <img src="//api.qrserver.com/v1/create-qr-code/?size=150x150&data=https://luobiao320.gitee.io/dubbo/springboot%E4%B8%ADdubbo%E6%9C%8D%E5%8A%A1%E5%BC%95%E7%94%A8/" alt="微信分享二维码">
    </div>
</div>

<div id="share-mask"></div>  
  <ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/dubbo/" rel="tag">dubbo</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/springboot/" rel="tag">springboot</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/%E6%9C%8D%E5%8A%A1%E5%BC%95%E7%94%A8/" rel="tag">服务引用</a></li></ul>

    </footer>
  </div>

   
  <nav class="article-nav">
    
      <a href="/git/git%E5%91%BD%E4%BB%A4/" class="article-nav-link">
        <strong class="article-nav-caption">上一篇</strong>
        <div class="article-nav-title">
          
            git命令
          
        </div>
      </a>
    
    
      <a href="/dubbo/dubbo%E8%87%AA%E9%80%82%E5%BA%94%E6%8B%93%E5%B1%95/" class="article-nav-link">
        <strong class="article-nav-caption">下一篇</strong>
        <div class="article-nav-title">dubbo自适应拓展</div>
      </a>
    
  </nav>

   
<!-- valine评论 -->
<div id="vcomments-box">
  <div id="vcomments"></div>
</div>
<script src="//cdn1.lncld.net/static/js/3.0.4/av-min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/valine@1.4.14/dist/Valine.min.js"></script>
<script>
  new Valine({
    el: "#vcomments",
    app_id: "kHx6wceVXdEyiCrLm0sLAwoF-gzGzoHsz",
    app_key: "ArEdGk6wzHOhrXLiwmClhDNH",
    path: window.location.pathname,
    avatar: "monsterid",
    placeholder: "给我的文章加点评论吧~",
    recordIP: true,
  });
  const infoEle = document.querySelector("#vcomments .info");
  if (infoEle && infoEle.childNodes && infoEle.childNodes.length > 0) {
    infoEle.childNodes.forEach(function (item) {
      item.parentNode.removeChild(item);
    });
  }
</script>
<style>
  #vcomments-box {
    padding: 5px 30px;
  }

  @media screen and (max-width: 800px) {
    #vcomments-box {
      padding: 5px 0px;
    }
  }

  #vcomments-box #vcomments {
    background-color: #fff;
  }

  .v .vlist .vcard .vh {
    padding-right: 20px;
  }

  .v .vlist .vcard {
    padding-left: 10px;
  }
</style>

 
     
</article>

</section>
      <footer class="footer">
  <div class="outer">
    <ul>
      <li>
        Copyrights &copy;
        2020-2021
        <i class="ri-heart-fill heart_icon"></i> luobiao
      </li>
    </ul>
    <ul>
      <li>
        
        
        
        Powered by <a href="https://hexo.io" target="_blank">Hexo</a>
        <span class="division">|</span>
        Theme - <a href="https://github.com/Shen-Yu/hexo-theme-ayer" target="_blank">Ayer</a>
        
      </li>
    </ul>
    <ul>
      <li>
        
      </li>
    </ul>
    <ul>
      
        <li>
          <a href="http://beian.miit.gov.cn/" target="_black" rel="nofollow">赣ICP备2020014719号-1</a>
        </li>
        
    </ul>
    <ul>
      
    </ul>
    <ul>
      <li>
        <!-- cnzz统计 -->
        
      </li>
    </ul>
  </div>
</footer>
      <div class="float_btns">
        <div class="totop" id="totop">
  <i class="ri-arrow-up-line"></i>
</div>

<div class="todark" id="todark">
  <i class="ri-moon-line"></i>
</div>

      </div>
    </main>
    <aside class="sidebar on">
      <button class="navbar-toggle"></button>
<nav class="navbar">
  
  <div class="logo">
    <a href="/"><img src="http://img.luobiao.vip/53119d0bda706bbe350333137a67ea3e_1.jpg" alt="一曲秋殇"></a>
  </div>
  
  <ul class="nav nav-main">
    
    <li class="nav-item">
      <a class="nav-item-link" href="/">主页</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/categories">分类</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/archives">归档</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/tags">标签</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/tags/dubbo">dubbo</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/tags/spring">spring</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/tags/mysql">mysql</a>
    </li>
    
  </ul>
</nav>
<nav class="navbar navbar-bottom">
  <ul class="nav">
    <li class="nav-item">
      
      <a class="nav-item-link nav-item-search"  title="Search">
        <i class="ri-search-line"></i>
      </a>
      
      
    </li>
  </ul>
</nav>
<div class="search-form-wrap">
  <div class="local-search local-search-plugin">
  <input type="search" id="local-search-input" class="local-search-input" placeholder="Search...">
  <div id="local-search-result" class="local-search-result"></div>
</div>
</div>
    </aside>
    <script>
      if (window.matchMedia("(max-width: 768px)").matches) {
        document.querySelector('.content').classList.remove('on');
        document.querySelector('.sidebar').classList.remove('on');
      }
    </script>
    <div id="mask"></div>

<!-- #reward -->
<div id="reward">
  <span class="close"><i class="ri-close-line"></i></span>
  <p class="reward-p"><i class="ri-cup-line"></i>请我喝杯咖啡吧~</p>
  <div class="reward-box">
    
    <div class="reward-item">
      <img class="reward-img" src="https://cdn.jsdelivr.net/gh/Shen-Yu/cdn/img/alipay.jpg">
      <span class="reward-type">支付宝</span>
    </div>
    
    
    <div class="reward-item">
      <img class="reward-img" src="https://cdn.jsdelivr.net/gh/Shen-Yu/cdn/img/wechat.jpg">
      <span class="reward-type">微信</span>
    </div>
    
  </div>
</div>
    
<script src="/js/jquery-2.0.3.min.js"></script>


<script src="/js/lazyload.min.js"></script>

<!-- Tocbot -->


<script src="/js/tocbot.min.js"></script>

<script>
  tocbot.init({
    tocSelector: '.tocbot',
    contentSelector: '.article-entry',
    headingSelector: 'h1, h2, h3, h4, h5, h6',
    hasInnerContainers: true,
    scrollSmooth: true,
    scrollContainer: 'main',
    positionFixedSelector: '.tocbot',
    positionFixedClass: 'is-position-fixed',
    fixedSidebarOffset: 'auto'
  });
</script>

<script src="https://cdn.jsdelivr.net/npm/jquery-modal@0.9.2/jquery.modal.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-modal@0.9.2/jquery.modal.min.css">
<script src="https://cdn.jsdelivr.net/npm/justifiedGallery@3.7.0/dist/js/jquery.justifiedGallery.min.js"></script>

<script src="/dist/main.js"></script>

<!-- ImageViewer -->

<!-- Root element of PhotoSwipe. Must have class pswp. -->
<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">

    <!-- Background of PhotoSwipe. 
         It's a separate element as animating opacity is faster than rgba(). -->
    <div class="pswp__bg"></div>

    <!-- Slides wrapper with overflow:hidden. -->
    <div class="pswp__scroll-wrap">

        <!-- Container that holds slides. 
            PhotoSwipe keeps only 3 of them in the DOM to save memory.
            Don't modify these 3 pswp__item elements, data is added later on. -->
        <div class="pswp__container">
            <div class="pswp__item"></div>
            <div class="pswp__item"></div>
            <div class="pswp__item"></div>
        </div>

        <!-- Default (PhotoSwipeUI_Default) interface on top of sliding area. Can be changed. -->
        <div class="pswp__ui pswp__ui--hidden">

            <div class="pswp__top-bar">

                <!--  Controls are self-explanatory. Order can be changed. -->

                <div class="pswp__counter"></div>

                <button class="pswp__button pswp__button--close" title="Close (Esc)"></button>

                <button class="pswp__button pswp__button--share" style="display:none" title="Share"></button>

                <button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>

                <button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>

                <!-- Preloader demo http://codepen.io/dimsemenov/pen/yyBWoR -->
                <!-- element will get class pswp__preloader--active when preloader is running -->
                <div class="pswp__preloader">
                    <div class="pswp__preloader__icn">
                        <div class="pswp__preloader__cut">
                            <div class="pswp__preloader__donut"></div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
                <div class="pswp__share-tooltip"></div>
            </div>

            <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
            </button>

            <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
            </button>

            <div class="pswp__caption">
                <div class="pswp__caption__center"></div>
            </div>

        </div>

    </div>

</div>

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/default-skin/default-skin.min.css">
<script src="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe-ui-default.min.js"></script>

<script>
    function viewer_init() {
        let pswpElement = document.querySelectorAll('.pswp')[0];
        let $imgArr = document.querySelectorAll(('.article-entry img:not(.reward-img)'))

        $imgArr.forEach(($em, i) => {
            $em.onclick = () => {
                // slider展开状态
                // todo: 这样不好，后面改成状态
                if (document.querySelector('.left-col.show')) return
                let items = []
                $imgArr.forEach(($em2, i2) => {
                    let img = $em2.getAttribute('data-idx', i2)
                    let src = $em2.getAttribute('data-target') || $em2.getAttribute('src')
                    let title = $em2.getAttribute('alt')
                    // 获得原图尺寸
                    const image = new Image()
                    image.src = src
                    items.push({
                        src: src,
                        w: image.width || $em2.width,
                        h: image.height || $em2.height,
                        title: title
                    })
                })
                var gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, {
                    index: parseInt(i)
                });
                gallery.init()
            }
        })
    }
    viewer_init()
</script>

<!-- MathJax -->

<!-- Katex -->

<!-- busuanzi  -->

<!-- ClickLove -->

<!-- ClickBoom1 -->

<!-- ClickBoom2 -->

<!-- CodeCopy -->


<link rel="stylesheet" href="/css/clipboard.css">

<script src="https://cdn.jsdelivr.net/npm/clipboard@2/dist/clipboard.min.js"></script>
<script>
  function wait(callback, seconds) {
    var timelag = null;
    timelag = window.setTimeout(callback, seconds);
  }
  !function (e, t, a) {
    var initCopyCode = function(){
      var copyHtml = '';
      copyHtml += '<button class="btn-copy" data-clipboard-snippet="">';
      copyHtml += '<i class="ri-file-copy-2-line"></i><span>COPY</span>';
      copyHtml += '</button>';
      $(".highlight .code pre").before(copyHtml);
      $(".article pre code").before(copyHtml);
      var clipboard = new ClipboardJS('.btn-copy', {
        target: function(trigger) {
          return trigger.nextElementSibling;
        }
      });
      clipboard.on('success', function(e) {
        let $btn = $(e.trigger);
        $btn.addClass('copied');
        let $icon = $($btn.find('i'));
        $icon.removeClass('ri-file-copy-2-line');
        $icon.addClass('ri-checkbox-circle-line');
        let $span = $($btn.find('span'));
        $span[0].innerText = 'COPIED';
        
        wait(function () { // 等待两秒钟后恢复
          $icon.removeClass('ri-checkbox-circle-line');
          $icon.addClass('ri-file-copy-2-line');
          $span[0].innerText = 'COPY';
        }, 2000);
      });
      clipboard.on('error', function(e) {
        e.clearSelection();
        let $btn = $(e.trigger);
        $btn.addClass('copy-failed');
        let $icon = $($btn.find('i'));
        $icon.removeClass('ri-file-copy-2-line');
        $icon.addClass('ri-time-line');
        let $span = $($btn.find('span'));
        $span[0].innerText = 'COPY FAILED';
        
        wait(function () { // 等待两秒钟后恢复
          $icon.removeClass('ri-time-line');
          $icon.addClass('ri-file-copy-2-line');
          $span[0].innerText = 'COPY';
        }, 2000);
      });
    }
    initCopyCode();
  }(window, document);
</script>


<!-- CanvasBackground -->


    
  </div>
</body>

</html>